#!/usr/bin/env python
"""
Synopsis:
    PHoneyC: Pure python honeyclient implementation.

Usage:
    python phoneyc.py [ options ] url

Options:
    -h              , --help                        Display this help information.
    -l <filename>   , --logfile=<filename>          Output file name for logs.
    -v              , --verbose                     Explain what is being done (DEBUG mode).
    -d <debuglevel> , --debug=<debuglevel>          Debug Level, 1-10.
    -r              , --retrieval-all               Retrieval all inline linking data.
    -c              , --cache-response              Cache the responses from the remote sites.
    -u <personality>, --user-agent=<personality>    Select a user agent (see below for values, default: 2)
    -n              , --replace-nonascii            Replace all non-ASCII characters with spaces(0x20) in all HTML or JS contents
    -m              , --universal-activex           Enable Universal ActiveX object 
"""

import sys, os, shutil 
import pycurl
import hashlib
import site
import getopt
from binascii import hexlify
from datetime import *
from graphGenerator import *

site.addsitedir('lib/python')

import config
import magic

DOWNLOADS_DIR = "log/downloads"
BINARIES_DIR  = "%s/binaries" % (DOWNLOADS_DIR, )
PDF_DIR       = "%s/pdf"      % (DOWNLOADS_DIR, )
APPLET_DIR    = "%s/applet"   % (DOWNLOADS_DIR, )
MISC_DIR      = "%s/misc"     % (DOWNLOADS_DIR, )

LOGDIRS       = (BINARIES_DIR, 
                 PDF_DIR,
                 APPLET_DIR,
                 MISC_DIR)

DOWNLOADS_STR = ["data", ]

USAGE_TEXT = __doc__

def usage():
    print USAGE_TEXT
    print "User Agents:"
    for ua in config.UserAgents:
        print "    [%2d] %s" % (ua[0], ua[1], )
    print ""
    sys.exit(1)

def check_logdirs():
    for logdir in LOGDIRS:
        if not os.access(logdir, os.F_OK):
            try:
                os.makedirs(logdir)
            except OSError:
                pass

def download(url):
    f = hashlib.md5()
    f.update(url)
    filename = "%s/%s" % (BINARIES_DIR, f.hexdigest(), )

    fd = open(filename, 'wb')
    ua = config.userAgent
    
    c = pycurl.Curl()
    c.setopt(pycurl.FOLLOWLOCATION, 1)
    c.setopt(pycurl.URL, str(url))
    c.setopt(pycurl.WRITEDATA, fd)
    c.setopt(pycurl.USERAGENT, ua)

    try:
        c.perform()
        code = c.getinfo(pycurl.HTTP_CODE) 
        if code == 404:
            config.VERBOSE(config.VERBOSE_DEBUG,
                           "[DEBUG] 404 File Not Found: "+url)
            fd.close()
            os.remove(filename)
            return
    except:
        import traceback
        traceback.print_exc(file = sys.stderr)
        sys.stderr.flush()

    c.close()
    fd.close()

    statinfo = os.stat(filename)
    if not statinfo.st_size:
        os.remove(filename)
        return

    fd = open(filename, 'r')
    h = hashlib.md5()
    h.update(fd.read())
    newfilename = "%s/%s" % (BINARIES_DIR, h.hexdigest(), )
    shutil.move(filename, newfilename)
    fd.close()

def report(alerts):
    for alert in alerts:
        print "\n===================================="
        if alert.atype == "ALERT_SHELLCODE":
            print "|--------AID:" + str(alert.aid) + "----------"
            print "|ATYPE:"       + str(alert.atype)
            print "|MESSAGE:"     + str(alert.msg)
            print "|MISC:"        + str(alert.misc)
            print "|LENGTH:"      + str(len(alert.shellcode))
            print "|SHELLCODE:"
            print hexlify(alert.shellcode)
            print "|Now run it:"
            shellcoderesult = alert.run_shellcode()
            print str(shellcoderesult)
            for item in shellcoderesult:
                if item['name'] == 'URLDownloadToFile':
                    url = item['arguments'][1][2][2]
                    print "Downloading from URL: %s" % url
                    download(url)

        if alert.atype == "ALERT_HEAPSPRAY" and alert.entropy < 1:
            print "|--------AID:" + str(alert.aid) + "----------"
            print "|ATYPE:"       + str(alert.atype)
            print "|MESSAGE:"     + str(alert.msg)
            print "|HIT:"         + str(alert.hit)
            print "|MEMUSAGE:"    + str(alert.memusage)
            print "|LENGTH:"      + str(alert.length)
            print "|ENTROPY:"     + str(alert.entropy)
            print "|MISC:"        + str(alert.misc)

def unique(seq):
    seen = set()
    seen_add = seen.add
    return [ x for x in seq if x not in seen and not seen_add(x)]

# Converts the tree in config.tree to XML
def tree_to_xml():
    # Start the string that will hold the XML
    landingsite = config.tree[0]

    # Get the link paths; must be unique
    paths = config.tree[1:]
    urls  = []
    
    for path in paths:
        for url in path:
            urls.append(url)

    # All the locations seen - also used as a lookup for path IDs
    urls = unique(urls)

    # Generate the resources
    count     = 1
    resources = ""
    for url in urls:
        resources += "\t\t<_" + str(count) + ">" + url + "</_" + str(count) + ">\n"
        count = count + 1

    # Generate edges
    count  = 1
    edges = ""

    for path in paths:
        # Get the number of urls in this path
        path_length = len(path)

        # Otherwise, if urls > 1, we create multiple xml entries for each node, linking from the previous
        # this path is the result of a redirect
        #el
        #if path_length > 1:
        # We check to see if this is a redirect path from the landing site
        cause = None
        if path[0] == config.initial_URL:
            config.landing_redirect = path[len(path)-1]
            cause = "Server Redirection"

        else:
            path.insert(0, config.landing_redirect)
            cause = "External File"

        path.reverse()          
        previous_url   = path.pop()
        print "URL: " + previous_url
        previous_index = urls.index(previous_url)

        for i in range(len(path)):
            next_url       = path.pop()
            next_index     = urls.index(next_url)
            edges += "\t\t<_" + str(count) + ">\n"
            edges += "\t\t\t<link>" + str(previous_index+1) + "," + str(next_index+1) + "</link>\n"
            edges += "\t\t\t<cause>" + cause + "</cause>\n"
            edges += "\t\t</_" + str(count) + ">\n"
            count          = count + 1
            previous_index = next_index

        previous_index = None

    xml =  "<output>\n"
    xml += "\t<timestamp>" + str(datetime.now()) + "</timestamp>\n"
    xml += "\t<useragent>" + config.userAgent + "</useragent>\n"
    xml += "\t<landingsite>" + landingsite + "</landingsite>\n"
    xml += "\t<resources>\n"
    xml += resources
    xml += "\t</resources>\n"
    xml += "\t<edges>\n"
    xml += edges + "\n"
    xml += "\t</edges>\n"
    xml += "</output>"  

    print xml

    # Save XML locally    
    h = hashlib.md5()
    h.update(config.initial_URL)
    filename  = config.appCodeName
    filename  = filename.replace(" ", "_") + ".xml"
    f         = open("log/" + filename, 'w')
    f.write(xml)
    f.close()

    return xml

if __name__ == "__main__":
    args = sys.argv[1:]
    try:
        options, args = getopt.getopt(args, 'hu:l:vd:rcnm',
            ['help', 
             'user-agent=', 
             'logfile=', 
             'verbose',
             'debug=', 
             'retrieval-all',
             'cache-response',
             'replace-nonascii',
             'universal-activex'
            ])
    except getopt.GetoptError, exp:
        usage()

    if not options and not args:
        usage()

    for option in options:
        if option[0] == '-h' or option[0] == '--help':
            usage()
        if option[0] == '-u' or option[0] == '---user-agent':
            for ua in config.UserAgents:
                if option[1] == str(ua[0]):
                    config.userAgent    = str(ua[2])
                    config.appCodeName  = str(ua[3])
                    config.appName      = str(ua[4])
                    config.appVersion   = str(ua[5])
                    config.browserTag   = str(ua[6])
        if option[0] == '-l' or option[0] == '--logfile':
            config.logfilename = option[1]
        if option[0] == '-v' or option[0] == '--verbose':
            config.verboselevel = 1
        if option[0] == '-d' or option[0] == '--debug':
            config.verboselevel = int(option[1])
        if option[0] == '-r' or option[0] == '--retrieval-all':
            config.retrieval_all = True
        if option[0] == '-c' or option[0] == '--cache-response':
            config.cache_response = True
        if option[0] == '-n' or option[0] == '--replace-nonascii':
            config.replace_nonascii = True
        if option[0] == '-m' or option[0] == '--universal-activex':
            config.universal_activex = True

        if config.verboselevel >= config.VERBOSE_DEBUG:
            config.universal_activex = True

    config.initial_URL = args[0]
    
    check_logdirs()

    from DOM.DOM import DOM
    config.tree.append(config.initial_URL)
    phoneycdom = DOM(config.initial_URL)
    alerts = phoneycdom.analyze()

    if alerts:
        report(alerts)
    else:
        print "No Shellcode/Heapspray Alerts."

    binaries_dir = os.listdir(BINARIES_DIR)
    for file in binaries_dir:
        filename = "%s/%s" % (BINARIES_DIR, file,)
        newname  = "%s/%s" % (MISC_DIR, file, )
        if magic.file(filename) in DOWNLOADS_STR:
            shutil.move(filename, newname)

    # GENERATE THE INFECTION TRAIL
    xml_str = tree_to_xml()
    generateGraph(xml_str)
